package com.kitten.note.biz.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.nacos.shaded.com.google.common.base.Preconditions;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.Lists;
import com.kitten.framework.common.exception.BizException;
import com.kitten.framework.common.response.Response;
import com.kitten.framework.common.utils.DateUtils;
import com.kitten.framework.common.utils.JsonUtils;
import com.kitten.framework.context.holder.LoginUserContextHolder;
import com.kitten.note.biz.config.ThreadPoolConfig;
import com.kitten.note.biz.constants.MQConstants;
import com.kitten.note.biz.constants.RedisKeyConstants;
import com.kitten.note.biz.convert.NoteConvert;
import com.kitten.note.biz.domain.dataobject.NoteCollectionDO;
import com.kitten.note.biz.domain.dataobject.NoteDO;
import com.kitten.note.biz.domain.dataobject.NoteLikeDO;
import com.kitten.note.biz.domain.mapper.NoteCollectionDOMapper;
import com.kitten.note.biz.domain.mapper.NoteDOMapper;
import com.kitten.note.biz.domain.mapper.NoteLikeDOMapper;
import com.kitten.note.biz.domain.mapper.TopicDOMapper;
import com.kitten.note.biz.enums.*;
import com.kitten.note.biz.model.dto.CollectUnCollectNoteMqDTO;
import com.kitten.note.biz.model.dto.LikeUnLikeNoteMqDTO;
import com.kitten.note.biz.model.dto.NoteOperateMqDTO;
import com.kitten.note.biz.model.dto.PublishNoteDTO;
import com.kitten.note.biz.model.vo.*;
import com.kitten.note.biz.rpc.DistributedIdGeneratorRpcService;
import com.kitten.note.biz.rpc.KeyValueRpcService;
import com.kitten.note.biz.rpc.UserRpcService;
import com.kitten.note.biz.service.NoteService;
import com.kitten.user.dto.resp.FindUserByIdRspDTO;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

import static com.kitten.note.biz.enums.ResponseCodeEnum.NOTE_NOT_LIKED;

/**
 * 笔记业务
 */
@Slf4j
@Service
public class NoteServiceImpl implements NoteService {
    @Resource
    private NoteDOMapper noteDOMapper;
    @Resource
    private TopicDOMapper topicDOMapper;
    @Resource
    private NoteLikeDOMapper noteLikeDOMapper;
    @Resource
    private NoteCollectionDOMapper noteCollectionDOMapper;
    @Resource
    private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
    @Resource
    private KeyValueRpcService keyValueRpcService;
    @Resource
    private UserRpcService userRpcService;
    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor taskExecutor;
    @Resource   // redis
    private RedisTemplate<String, String> redisTemplate;
    @Resource   // rocketMQ
    private RocketMQTemplate rocketMQTemplate;


    /**
     * 笔记详情本地缓存
     */
    private static final Cache<Long, String> LOCAL_CACHE = Caffeine.newBuilder()
            .initialCapacity(10000) // 设置初始容量为 10000 个条目
            .maximumSize(10000) // 设置缓存的最大容量为 10000 个条目
            .expireAfterWrite(1, TimeUnit.HOURS) // 设置缓存条目在写入后 1 小时过期
            .build();
    @Autowired
    private ThreadPoolConfig threadPoolConfig;

    /**
     * 发布笔记
     * @param publishNoteReqVO
     * @return
     */
    @Override
    public Response<?> publishNote(PublishNoteReqVO publishNoteReqVO) {
        // 笔记类型
        Integer type = publishNoteReqVO.getType();
        // 获取对应类型的枚举
        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
        // 若非图文、视频，抛出业务业务异常
        if (Objects.isNull(noteTypeEnum)) {
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }
        String imgUris = null;
        // 笔记内容是否为空，默认值为 true，即空
        Boolean isContentEmpty = true;
        String videoUri = null;
        switch (noteTypeEnum) {
            case IMAGE_TEXT: // 图文笔记
                List<String> imgUriList = publishNoteReqVO.getImgUris();
                // 校验图片是否为空
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
                // 校验图片数量
                Preconditions.checkArgument(imgUriList.size() <= 8, "笔记图片不能多于 8 张");
                // 将图片链接拼接，以逗号分隔
                imgUris = StringUtils.join(imgUriList, ",");
                break;
            case VIDEO: // 视频笔记
                videoUri = publishNoteReqVO.getVideoUri();
                // 校验视频链接是否为空
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");
                break;
            default:
                break;
        }
        // RPC: 调用分布式 ID 生成服务，生成笔记 ID
        String snowflakeIdId = distributedIdGeneratorRpcService.getSnowflakeId();
        // 笔记内容 UUID
        String contentUuid = null;
        // 笔记内容
        String content = publishNoteReqVO.getContent();
        // 若用户填写了笔记内容
        if (StringUtils.isNotBlank(content)) {
            // 内容是否为空，置为 false，即不为空
            isContentEmpty = false;
            // 生成笔记内容 UUID
            contentUuid = UUID.randomUUID().toString();

            /**
             * // RPC: 调用 KV 键值服务，存储短文本
             * boolean isSavedSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
             *             if (!isSavedSuccess) {
             *                 throw new BizException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
             *             }
             */
        }
        // 话题
        Long topicId = publishNoteReqVO.getTopicId();
        String topicName = null;
        if (Objects.nonNull(topicId)) {
            // 获取话题名称
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);
        }
        // 发布者用户 ID
        Long creatorId = LoginUserContextHolder.getUserId();

        // 构建笔记 DO 对象
        NoteDO noteDO = NoteDO.builder()
                .id(Long.valueOf(snowflakeIdId))
                .isContentEmpty(isContentEmpty)
                .creatorId(creatorId)
                .imgUris(imgUris)
                .title(publishNoteReqVO.getTitle())
                .topicId(publishNoteReqVO.getTopicId())
                .topicName(topicName)
                .type(type)
                .visible(NoteVisibleEnum.PUBLIC.getCode())
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .status(NoteStatusEnum.NORMAL.getCode())
                .isTop(Boolean.FALSE)
                .videoUri(videoUri)
                .contentUuid(contentUuid)
                .build();
        // 笔记正文内容为空，由于不用向 KV服务存储短文本，所以不用发事务消息
        if (StringUtils.isBlank(content)) {
            processPublishContentEmptyNote(creatorId, noteDO, snowflakeIdId);
            return Response.success();
        }
        // 发送事务消息
        PublishNoteDTO publishNoteDTO = NoteConvert.INSTANCE.convertDO2DTO(noteDO);
        publishNoteDTO.setContent(content);
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(publishNoteDTO)).build();
        TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction(MQConstants.TOPIC_PUBLISH_NOTE_TRANSACTION, message, null);
        log.info("=====> 事务消息发送结果: {}", transactionSendResult.getLocalTransactionState());


        return Response.success();
    }

    // 处理发布内容为空的情况
    private void processPublishContentEmptyNote(Long creatorId, NoteDO noteDO, String snowflakeIdId) {
        // 删除个人主页 - 已发布笔记列表缓存
        // 应采取灵活的策略，如果是大V, 应该直接更新缓存，而不是直接删除；普通用户则可直接删除
        String publishedNoteListRedisKey = RedisKeyConstants.buildPublishedNoteListKey(creatorId);
        redisTemplate.delete(publishedNoteListRedisKey);
        // 笔记入库
        noteDOMapper.insert(noteDO);
        // 延迟双删, 发送延迟消息
        sendDelayDeleteRedisPublishedNoteListCacheMQ(creatorId);

        NoteOperateMqDTO noteOperateMqDTO = NoteOperateMqDTO.builder()
                .creatorId(creatorId)
                .noteId(Long.valueOf(snowflakeIdId))
                .type(NoteOperateEnum.PUBLISH.getCode()) // 发布笔记
                .build();
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(noteOperateMqDTO)).build();
        String destinations = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_PUBLISH;
        rocketMQTemplate.asyncSend(destinations, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 发布笔记 MQ 发送成功, {}", sendResult);
            }

            @Override
            public void onException(Throwable e) {
                log.error("==> 发布笔记 MQ 发送失败, {}", e);
            }
        });

    }
    // 发送延迟双删, 发送延迟消息
    private void sendDelayDeleteRedisPublishedNoteListCacheMQ(Long creatorId) {
        // ...
    }

    /**
     * 查询笔记详情
     *
     * @param findNoteDetailReqVO
     * @return
     */
    @Override
    @SneakyThrows
    public Response<FindNoteDetailRspVO> findNoteDetail(FindNoteDetailReqVO findNoteDetailReqVO) {
        Long noteId = findNoteDetailReqVO.getId();
        Long userId = LoginUserContextHolder.getUserId();
        // 1.先从本地缓存中获取
        String findNoteDetailRspVOStrLocalCache = LOCAL_CACHE.getIfPresent(noteId);
        if (StringUtils.isNotBlank(findNoteDetailRspVOStrLocalCache)) {
            FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOStrLocalCache, FindNoteDetailRspVO.class);
            log.info("==> 命中本地缓存, 笔记详情: {}", findNoteDetailRspVO);
            checkNoteVisibleFromVO(userId, findNoteDetailRspVO);    // 可见性校验
            return Response.success(findNoteDetailRspVO);
        }

        // 2.从redis缓存中获取
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        String noteDetailJson = redisTemplate.opsForValue().get(noteDetailRedisKey);
        if (StringUtils.isNotBlank(noteDetailJson)) {   // 命中redis缓存
            FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(noteDetailJson, FindNoteDetailRspVO.class);
            // 异步线程将缓存数据存入本地缓存
            taskExecutor.submit(() -> {
                LOCAL_CACHE.put(noteId, Objects.isNull(findNoteDetailRspVO) ? "null" : JsonUtils.toJsonString(findNoteDetailRspVO));
            });
            // 可见性校验
            checkNoteVisibleFromVO(userId, findNoteDetailRspVO);

            return Response.success(findNoteDetailRspVO);
        }

        // 3.redis取不到就查数据库: 查询笔记详情
        NoteDO noteDO = noteDOMapper.selectByPrimaryKey(noteId);
        // 若笔记不存在，抛出业务异常
        if (Objects.isNull(noteDO)) {
            taskExecutor.execute(() -> {
                // 防止缓存穿透, 将空数据存入Redis
                long expireTime = 60 + RandomUtil.randomInt(60);
                redisTemplate.opsForValue().set(noteDetailRedisKey, "null", expireTime, TimeUnit.SECONDS);
            });
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        // 可见性校验
        Integer visible = noteDO.getVisible();
        checkNoteVisible(visible, userId, noteDO.getCreatorId());


        // 4.并发查询优化
        // RPC: 调用用户服务
        Long creatorId = noteDO.getCreatorId();
        CompletableFuture<FindUserByIdRspDTO> userResultFuture = CompletableFuture.supplyAsync(() -> userRpcService.findById(creatorId), taskExecutor);

        // RPC: 调用 K-V 存储服务获取内容
        CompletableFuture<String> contentResultFuture = CompletableFuture.completedFuture(null);
        if (Objects.equals(noteDO.getIsContentEmpty(), Boolean.FALSE)) {
            contentResultFuture = CompletableFuture.supplyAsync(() -> keyValueRpcService.findNoteContent(noteDO.getContentUuid()), taskExecutor);
        }
        CompletableFuture<String> finalContentResultFuture = contentResultFuture;
        CompletableFuture<FindNoteDetailRspVO> resultFuture = CompletableFuture
                .allOf(userResultFuture, contentResultFuture)
                .thenApply(s -> {
                    // 获取 Future 返回的结果
                    FindUserByIdRspDTO findUserByIdRspDTO = userResultFuture.join();
                    String content = finalContentResultFuture.join();
                    // 笔记类型
                    Integer noteType = noteDO.getType();
                    // 图文笔记图片链接(字符串)
                    String imgUrisStr = noteDO.getImgUris();
                    // 图文笔记图片链接(集合)
                    List<String> imgUris = null;
                    // 如果查询的是图文笔记，需要将图片链接的逗号分隔开，转换成集合
                    if (Objects.equals(noteType, NoteTypeEnum.IMAGE_TEXT.getCode())
                            && StringUtils.isNotBlank(imgUrisStr)) {
                        imgUris = List.of(imgUrisStr.split(","));
                    }
                    // 构建返参 VO 实体类
                    return FindNoteDetailRspVO.builder()
                            .id(noteDO.getId())
                            .type(noteDO.getType())
                            .title(noteDO.getTitle())
                            .content(content)
                            .imgUris(imgUris)
                            .topicId(noteDO.getTopicId())
                            .topicName(noteDO.getTopicName())
                            .creatorId(noteDO.getCreatorId())
                            .creatorName(findUserByIdRspDTO.getNickName())
                            .avatar(findUserByIdRspDTO.getAvatar())
                            .videoUri(noteDO.getVideoUri())
                            .updateTime(noteDO.getUpdateTime())
                            .visible(noteDO.getVisible())
                            .build();
                });
        FindNoteDetailRspVO findNoteDetailRspVO = resultFuture.get();
        // 异步线程中将笔记详情存入 Redis
        taskExecutor.submit(() -> {
            String noteDetailJson1 = JsonUtils.toJsonString(findNoteDetailRspVO);
            // 过期时间（保底1天 + 随机秒数，将缓存过期时间打散，防止同一时间大量缓存失效，导致数据库压力太大）
            long expireSeconds = 60 + RandomUtil.randomInt(60);
            redisTemplate.opsForValue().set(noteDetailRedisKey, noteDetailJson1, expireSeconds, TimeUnit.SECONDS);
        });
        return Response.success(findNoteDetailRspVO);
    }

    /**
     * 更新笔记
     *
     * @param updateNoteReqVO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO) {
        // 笔记 ID
        Long noteId = updateNoteReqVO.getId();
        // 笔记类型
        Integer type = updateNoteReqVO.getType();
        // 获取对应类型的枚举
        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
        // 若非图文、视频，抛出业务业务异常
        if (Objects.isNull(noteTypeEnum)) {
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }
        String imgUris = null;
        String videoUri = null;
        switch (noteTypeEnum) {
            case IMAGE_TEXT: // 图文笔记
                List<String> imgUriList = updateNoteReqVO.getImgUris();
                // 校验图片是否为空
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
                // 校验图片数量
                Preconditions.checkArgument(imgUriList.size() <= 8, "笔记图片不能多于 8 张");

                imgUris = StringUtils.join(imgUriList, ",");
                break;
            case VIDEO: // 视频笔记
                videoUri = updateNoteReqVO.getVideoUri();
                // 校验视频链接是否为空
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");
                break;
            default:
                break;
        }
        // 鉴权
        Long currUserId = LoginUserContextHolder.getUserId();
        NoteDO selectNoteDO = noteDOMapper.selectByPrimaryKey(noteId);
        if (Objects.isNull(selectNoteDO)) { // 笔记不存在
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }
        if (Objects.isNull(selectNoteDO)) { // 不存在该笔记
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }


        // 话题
        Long topicId = updateNoteReqVO.getTopicId();
        log.info("==> topicId: {}", topicId);
        String topicName = null;
        if (Objects.nonNull(topicId)) {
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);

            // 判断一下提交的话题, 是否是真实存在的
            if (StringUtils.isBlank(topicName)) throw new BizException(ResponseCodeEnum.TOPIC_NOT_FOUND);
        }
        // 更新笔记元数据表 t_note
        String content = updateNoteReqVO.getContent();
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isContentEmpty(StringUtils.isBlank(content))
                .imgUris(imgUris)
                .title(updateNoteReqVO.getTitle())
                .topicId(updateNoteReqVO.getTopicId())
                .topicName(topicName)
                .type(type)
                .updateTime(LocalDateTime.now())
                .videoUri(videoUri)
                .build();

        noteDOMapper.updateByPrimaryKey(noteDO);
        // 删除 Redis 缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);
        // 删除本地缓存 todo : MQ 通知其他节点删除缓存
//        LOCAL_CACHE.invalidate(noteId);
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("==> 笔记更新成功, 通知 MQ 删除本地缓存");


        // 笔记内容更新
        // 查询此篇笔记内容对应的 UUID
        NoteDO noteDO1 = noteDOMapper.selectByPrimaryKey(noteId);
        String contentUuid = noteDO1.getContentUuid();
        // 笔记内容是否更新成功
        boolean isUpdateContentSuccess = false;
        if (StringUtils.isBlank(content)) {
            // 若笔记内容为空，则删除 K-V 存储
            isUpdateContentSuccess = keyValueRpcService.deleteNoteContent(contentUuid);
        } else {
            // 调用 K-V 更新短文本
            isUpdateContentSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
        }
        // 如果更新失败，抛出业务异常，回滚事务
        if (!isUpdateContentSuccess) {
            throw new BizException(ResponseCodeEnum.NOTE_UPDATE_FAIL);
        }
        return Response.success();
    }

    /**
     * 删除笔记
     *
     * @param deleteNoteReqVO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
        // 鉴权
        Long currUserId = LoginUserContextHolder.getUserId();
        Long noteId = deleteNoteReqVO.getId();
        NoteDO selectNoteDO = noteDOMapper.selectByPrimaryKey(noteId);
        if (Objects.isNull(selectNoteDO)) { // 笔记不存在
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }


        // 逻辑删除
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .status(NoteStatusEnum.DELETED.getCode())
                .updateTime(LocalDateTime.now())
                .build();

        int count = noteDOMapper.updateByPrimaryKeySelective(noteDO);

        // 若影响的行数为 0，则表示该笔记不存在
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        // 删除缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        // 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        // MQ 用户发布笔记数 计数
        NoteOperateMqDTO dto = NoteOperateMqDTO.builder()
                .creatorId(selectNoteDO.getCreatorId())
                .noteId(noteId)
                .type(NoteOperateEnum.DELETE.getCode())
                .build();
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(dto)).build();
        String destinations = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_DELETE;
        rocketMQTemplate.asyncSend(destinations, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 删除笔记 MQ 用户发布笔记数 计数 发送成功, {}", sendResult);
            }

            @Override
            public void onException(Throwable e) {
                log.error("==> 删除笔记 MQ 用户发布笔记数 计数 发送失败, {}", e);
            }
        });

        return Response.success();
    }

    /**
     * 笔记仅自己可见
     *
     * @param updateNoteVisibleOnlyMeReqVO
     * @return
     */
    @Override
    public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
        Long noteId = updateNoteVisibleOnlyMeReqVO.getId();

        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .visible(NoteVisibleEnum.PRIVATE.getCode())
                .updateTime(LocalDateTime.now())
                .build();
        int count = noteDOMapper.updateVisibleOnlyMe(noteDO);
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        // 删除Redis缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);
        // 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");
        return Response.success();
    }

    /**
     * 置顶笔记
     *
     * @param topNoteReqVO
     * @return
     */
    @Override
    public Response<?> topNote(TopNoteReqVO topNoteReqVO) {
        Long noteId = topNoteReqVO.getId();
        // 置顶
        // 当前登录用户ID
        Long userId = LoginUserContextHolder.getUserId();
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isTop(Boolean.TRUE)
                .updateTime(LocalDateTime.now())
                .creatorId(userId)
                .build();
        int count = noteDOMapper.updateIsTop(noteDO);
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }
        // 删除 Redis 缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        // 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");
        return Response.success();
    }

    /**
     * 删除笔记本地缓存
     *
     * @param noteId
     * @return
     */
    @Override
    public void deleteNoteLocalCache(Long noteId) {
        LOCAL_CACHE.invalidate(noteId);
    }

    /**
     * 校验笔记的可见性
     *
     * @param visible    是否可见
     * @param currUserId 当前用户 ID
     * @param creatorId  笔记创建者
     */
    private void checkNoteVisible(Integer visible, Long currUserId, Long creatorId) {
        if (Objects.equals(visible, NoteVisibleEnum.PRIVATE.getCode()) && !Objects.equals(currUserId, creatorId)) { // 仅自己可见, 并且访问用户为笔记创建者
            throw new BizException(ResponseCodeEnum.NOTE_PRIVATE);
        }
    }

    /**
     * 校验笔记的可见性（针对 VO 实体类）
     *
     * @param userId
     * @param findNoteDetailRspVO
     */
    private void checkNoteVisibleFromVO(Long userId, FindNoteDetailRspVO findNoteDetailRspVO) {
        if (Objects.nonNull(findNoteDetailRspVO)) {
            Integer visible = findNoteDetailRspVO.getVisible();
            checkNoteVisible(visible, userId, findNoteDetailRspVO.getCreatorId());
        }
    }

    /**
     * 点赞笔记
     * @param likeNoteReqVO
     * @return
     */
    @Override
    public Response<?> likeNote(LikeNoteReqVO likeNoteReqVO) {
        Long userId = LoginUserContextHolder.getUserId();
        Long noteId = likeNoteReqVO.getNoteId();
        //1. 本地Caffeine缓存+数据库 查询 校验笔记是否存在
        Long creatorId = checkNoteIsExistsAndGetAuthorId(noteId);
        //2. 校验用户是否已经点赞 (布隆过滤器查询key, 可能存在误判)
        String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);  // bloom:note:likes:2
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_like_check.lua")));
        script.setResultType(Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);   // 执行lua脚本
        NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);

        String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);    // user:note:likes:2
        switch (noteLikeLuaResultEnum) {    // 点赞的笔记id不存在时
            case NOT_EXISTS -> {  // 布隆过滤器不存在该用户点赞记录, 说明用户没有点赞过, 则新增一条记录
                // TODO: 数据库校验笔记是否被点赞, 并初始化布隆过滤器
                int count = noteLikeDOMapper.selectCountByUserIdAndNoteId(userId, noteId);
                // 半天 + 随机秒数
                long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);
                if (count > 0) {   // 数据库中存在点赞记录, 异步初始化布隆过滤器
                    taskExecutor.submit(() -> {
                        batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
                    });
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
                // 数据库中不存在当前笔记的点赞记录, 则同步初始化布隆过滤器, 并新增一条记录
                batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_like_and_expire.lua")));
                script.setResultType(Long.class);
                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId, expireSeconds);
            }
            case NOTE_LIKED -> {    // 笔记已经被点赞 TODO: 布隆过滤器误判 可能存在误判, 需要进一步校验
                Double score = redisTemplate.opsForZSet().score(userNoteLikeZSetKey, noteId);
                if (Objects.nonNull(score)) {
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
                // score 为空, 查询数据库校验
                int count = noteLikeDOMapper.selectCountByUserIdAndNoteId(userId, noteId);
                if (count > 0) {   // 数据库中存在点赞记录 TODO: 初始化ZSET
                    asyncInitUserNoteLikesZSet(userId, userNoteLikeZSetKey);
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
            }
        }
        //3. 更新用户 ZSET 点赞列表
        LocalDateTime now = LocalDateTime.now();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_like_check_and_update_zset.lua")));    // 校验笔记是否在ZSET中存在, 存在就更新点赞
        script.setResultType(Long.class);
        result = redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
        if (Objects.equals(result, NoteLikeLuaResultEnum.NOT_EXISTS.getCode())) {   // ZSET 未存在, 需要初始化
            // 数据库查询最近点赞的100条笔记, 批量添加到ZSET
            List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectLikedByUserIdAndLimit(userId, 100);
            long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);  // 12小时 + 随机秒数
            DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
            script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
            script2.setResultType(Long.class);
            if (CollUtil.isNotEmpty(noteLikeDOS)) {
                Object[] luaArgs = buildNoteLikeZSetLuaArgs(noteLikeDOS, expireSeconds);
                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs);    // 同步点赞记录(数据库)到ZSET
                redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now)); // 将点赞的笔记添加到ZSET
            } else {    // 数据库中没有点赞记录, 则初始化 ZSET 添加一条点赞记录
                List<Object> luaArgs = Lists.newArrayList();
                luaArgs.add(DateUtils.localDateTime2Timestamp(LocalDateTime.now()));    // score 时间戳
                luaArgs.add(noteId);
                luaArgs.add(expireSeconds);
                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs.toArray());
            }
        }
        //4. 发送MQ 点赞数据入库
        LikeUnLikeNoteMqDTO likeUnLikeNoteMqDTO = LikeUnLikeNoteMqDTO.builder()
                .userId(userId)
                .noteCreatorId(creatorId)
                .noteId(noteId)
                .type(LikeUnLikeNoteTypeEnum.LIKE.getCode())    // 点赞
                .createTime(LocalDateTime.now())
                .build();
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnLikeNoteMqDTO)).build();
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_LIKE; // TOPIC 携带 TAG
        String hashKey = String.valueOf(userId);
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("LikeOrUnlikeNote MQ 点赞数据 发送成功, message: {}", message);
            }

            @Override
            public void onException(Throwable throwable) {
                log.info("LikeOrUnlikeNote MQ 点赞数据 发送失败, error: {}", throwable);
            }
        });

        return Response.success();
    }

    /**
     * 取消点赞笔记
     * @param unlikeReqVO
     * @return
     */
    @Override
    public Response<?> unLikeNote(UnlikeReqVO unlikeReqVO) {
        Long noteId = unlikeReqVO.getNoteId();
        //1. 校验笔记是否存在
        Long creatorId = checkNoteIsExistsAndGetAuthorId(noteId);
        //2. 校验用户是否已经点赞 (布隆过滤器查询key, 可能存在误判)
        Long userId = LoginUserContextHolder.getUserId();
        String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_unlike_check.lua")));
        script.setResultType(Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);
        NoteUnlikeLuaResultEnum noteLikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result);
        switch (noteLikeLuaResultEnum) {
            case NOT_EXISTS -> { // bloom不存在该用户点赞记录
                // 数据库校验笔记是否被点赞, 并初始化布隆过滤器
                taskExecutor.submit(() -> {
                    long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);  // 12小时 + 随机秒数
                    batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
                });
                int count = noteLikeDOMapper.selectCountByUserIdAndNoteId(userId, noteId);
                if (count == 0) {   // 数据库中不存在当前笔记的点赞记录, 无法取消点赞
                    throw new BizException(ResponseCodeEnum.NOTE_NOT_LIKED);
                }
            }
            case NOTE_NOT_LIKE -> {
                throw new BizException(NOTE_NOT_LIKED);
            }
        }
        //3. 更新用户 ZSET 点赞列表 (删除列表中的该笔记记录)
        String deleteNoteLikeZsetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);  // user:note:likes:xxx
        redisTemplate.opsForZSet().remove(deleteNoteLikeZsetKey, noteId);
        //4. 发送MQ 取消点赞数据入库
        LikeUnLikeNoteMqDTO mqDTO = LikeUnLikeNoteMqDTO.builder()
                .userId(userId)
                .noteCreatorId(creatorId)
                .noteId(noteId)
                .type(LikeUnLikeNoteTypeEnum.UNLIKE.getCode())
                .createTime(LocalDateTime.now())
                .build();
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(mqDTO)).build();
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_UNLIKE; // TOPIC 携带 TAG
        String hashKey = String.valueOf(userId);
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("LikeOrUnlikeNote MQ 取消点赞数据 发送成功, message: {}", message);
            }

            @Override
            public void onException(Throwable throwable) {
                log.info("LikeOrUnlikeNote MQ 取消点赞数据 发送失败, error: {}", throwable);
            }
        });

        return Response.success();
    }

    /**
     * 收藏笔记
     * @param collectNoteReqVO
     * @return
     */
    @Override
    public Response<?> collectNote(CollectNoteReqVO collectNoteReqVO) {
        Long noteId = collectNoteReqVO.getNoteId();
        //1. 校验笔记是否存在
        Long creatorId = checkNoteIsExistsAndGetAuthorId(noteId);
        //2. 校验用户是否已经收藏
        Long userId = LoginUserContextHolder.getUserId();
        String bloomNoteCollectKey = RedisKeyConstants.buildBloomUserNoteCollectListKey(userId);    // bloom:note:collect:userId
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/collectNote/bloom_note_collect_check.lua")));
        script.setResultType(Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomNoteCollectKey), noteId);
        NoteCollectLuaResultEnum noteCollectLuaResultEnum = NoteCollectLuaResultEnum.valueOf(result);

        String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(userId);    // user:note:collect:userId
        switch (noteCollectLuaResultEnum) {
            case NOT_EXISTS -> { // bloom不存在该用户收藏记录
                int count = noteCollectionDOMapper.selectCountByUserIdAndNoteId(userId, noteId);
                long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);  // 12小时 + 随机秒数
                if (count > 0) {
                    //TODO: 异步初始化 bloom
                    taskExecutor.submit(() -> {
                        batchAddNoteCollection2BloomAndExpire(userId, expireSeconds, bloomNoteCollectKey);
                    });
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
                }
                // 数据库中不存在当前笔记的收藏记录, 则同步初始化布隆过滤器, 并新增一条记录
                batchAddNoteCollection2BloomAndExpire(userId, expireSeconds, bloomNoteCollectKey);
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/collectNote/bloom_add_note_collect_and_expire.lua")));
                script.setResultType(Long.class);
                redisTemplate.execute(script, Collections.singletonList(bloomNoteCollectKey), noteId, expireSeconds);
            }
            case NOTE_COLLECTED -> { // 笔记已经被收藏 (可能存在误判)
                Double score = redisTemplate.opsForZSet().score(userNoteCollectZSetKey, noteId);
                if (Objects.nonNull(score)) {
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
                }
                // score 为空, 查询数据库校验
                int count = noteCollectionDOMapper.selectCountByUserIdAndNoteId(userId, noteId);
                if (count > 0) {   // 数据库中存在收藏记录 TODO: 初始化ZSET
                    log.info("==> 笔记 {} 已经被收藏, 异步初始化 ZSET", noteId);
                    asyncInitUserNoteCollectZSet(userId, userNoteCollectZSetKey);
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
                }
            }
        }
        //3. ZSET 收藏列表更新
        LocalDateTime now = LocalDateTime.now();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/collectNote/note_collect_check_and_update_zset.lua")));
        script.setResultType(Long.class);
        result = redisTemplate.execute(script, Collections.singletonList(userNoteCollectZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
        if (Objects.equals(result, NoteCollectLuaResultEnum.NOT_EXISTS.getCode())) {     // ZSET 未存在, 需要初始化
            List<NoteCollectionDO> noteCollectionDOS = noteCollectionDOMapper.selectCollectedByUserIdAndLimit(userId, 100);
            long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);  // 12小时 + 随机秒数
            DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
            script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/collectNote/batch_add_note_collect_zset_and_expire.lua")));
            script2.setResultType(Long.class);
            if (CollUtil.isNotEmpty(noteCollectionDOS)) {
                Object[] luaArgs = buildNoteCollectZSETLuaArgs(noteCollectionDOS, expireSeconds);
                redisTemplate.execute(script2, Collections.singletonList(userNoteCollectZSetKey), luaArgs);    // 同步收藏记录(数据库)到ZSET
                redisTemplate.execute(script, Collections.singletonList(userNoteCollectZSetKey), noteId, DateUtils.localDateTime2Timestamp(now)); // 将收藏的笔记添加到ZSET
            } else {    // 数据库中没有收藏记录, 则初始化 ZSET 添加一条收藏记录
                List<Object> luaArgs = Lists.newArrayList();
                luaArgs.add(DateUtils.localDateTime2Timestamp(LocalDateTime.now()));    // score 时间戳
                luaArgs.add(noteId);
                luaArgs.add(expireSeconds);
                redisTemplate.execute(script2, Collections.singletonList(userNoteCollectZSetKey), luaArgs.toArray());
            }
        }

        //4. 发送MQ 收藏数据入库
        CollectUnCollectNoteMqDTO dto = CollectUnCollectNoteMqDTO.builder()
                .userId(userId)
                .noteCreatorId(creatorId)
                .noteId(noteId)
                .type(CollectUnCollectNoteTypeEnum.COLLECT.getCode())
                .createTime(LocalDateTime.now())
                .build();
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(dto)).build();
        String destination = MQConstants.TOPIC_COLLECT_OR_UNCOLLECT + ":" + MQConstants.TAG_COLLECT; // TOPIC 携带 TAG
        String hashKey = String.valueOf(userId);
        rocketMQTemplate.asyncSendOrderly(destination,message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("CollectOrUncollectNote MQ 收藏数据 发送成功, message: {}", message);
            }

            @Override
            public void onException(Throwable throwable) {
                 log.info("CollectOrUncollectNote MQ 收藏数据 发送失败, error: {}", throwable);
            }
        });

        return Response.success();
    }

    /**
     * 取消收藏笔记
     * @param unCollectNoteReqVO
     * @return
     */
    @Override
    public Response<?> unCollectNote(UnCollectNoteReqVO unCollectNoteReqVO) {
        // 笔记ID
        Long noteId = unCollectNoteReqVO.getNoteId();
        // 1. 校验笔记是否存在
        Long creatorId = checkNoteIsExistsAndGetAuthorId(noteId);
        //2. 校验笔记是否被收藏过
        Long userId = LoginUserContextHolder.getUserId();
        String bloomNoteCollectKey = RedisKeyConstants.buildBloomUserNoteCollectListKey(userId);    // bloom:note:collect:userId
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/collectNote/bloom_note_uncollect_check.lua")));
        script.setResultType(Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomNoteCollectKey), noteId);
        NoteUnCollectLuaResultEnum noteUnCollectLuaResultEnum = NoteUnCollectLuaResultEnum.valueOf(result);
        switch (noteUnCollectLuaResultEnum) {
            case NOT_EXISTS -> {    // bloom不存在该用户收藏记录
                taskExecutor.submit(() -> {
                    long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);  // 12小时 + 随机秒数
                    batchAddNoteCollection2BloomAndExpire(userId, expireSeconds, bloomNoteCollectKey);
                });
                int count = noteCollectionDOMapper.selectCountByUserIdAndNoteId(userId, noteId);
                if (count == 0) {   // 数据库中不存在当前笔记的收藏记录, 无法取消收藏
                    throw new BizException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
                }
            }
            case NOTE_UN_COLLECT -> {
                throw new BizException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
            }
        }
        //3. 删除 ZSET 中已收藏的笔记 ID
        String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(userId);    // user:note:collects:userId
        redisTemplate.opsForZSet().remove(userNoteCollectZSetKey, noteId);

        //4. 发送 MQ, 数据更新落库
        CollectUnCollectNoteMqDTO dto = CollectUnCollectNoteMqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .type(CollectUnCollectNoteTypeEnum.UNCOLLECT.getCode())
                .createTime(LocalDateTime.now())
                .build();
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(dto)).build();
        String destination = MQConstants.TOPIC_COLLECT_OR_UNCOLLECT + ":" + MQConstants.TAG_UNCOLLECT; // TOPIC 携带 TAG
        String hashKey = String.valueOf(userId);
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("CollectOrUncollectNote MQ 取消收藏数据 发送成功, message: {}", message);
            }

            @Override
            public void onException(Throwable throwable) {
                log.info("CollectOrUncollectNote MQ 取消收藏数据 发送失败, error: {}", throwable);
            }
        });

        return Response.success();
    }


    // 校验笔记是否存在 并返回 笔记创建者ID
    private Long checkNoteIsExistsAndGetAuthorId(Long noteId) {
        String findNoteDetailRspVOStrLocalCache = LOCAL_CACHE.getIfPresent(noteId);
        FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOStrLocalCache, FindNoteDetailRspVO.class);
        if (Objects.isNull(findNoteDetailRspVO)) { // 缓存中不存在, 查数据库
            log.info("校验笔记是否存在: 缓存不存在, 查询数据库, 笔记ID: {}", noteId);
            Long creatorId = noteDOMapper.selectCreatorIdByNoteId(noteId);
            int count = noteDOMapper.selectCountByNoteId(noteId);
            if (count == 0) { // 数据库中不存在, 抛出异常
                throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
            }
            // 数据库中存在, 缓存中不存在, 异步查询并缓存
            taskExecutor.submit(() -> {
                FindNoteDetailReqVO reqVO = FindNoteDetailReqVO.builder()
                        .id(noteId)
                        .build();
                findNoteDetail(reqVO);
            });
            return creatorId;
        }
        return findNoteDetailRspVO.getCreatorId();
    }

    /**
     * 初始化 笔记点赞布隆过滤器Key 并设置过期时间
     * 数据库查询用户点赞列表, 批量添加到布隆过滤器 (若无记录则不初始化)
     * @param userId
     * @param expireSeconds
     * @param bloomUserNoteLikeListKey
     */
    private void batchAddNoteLike2BloomAndExpire(Long userId, long expireSeconds, String bloomUserNoteLikeListKey) {
        try {
            //1. 异步全量同步用户点赞列表到布隆过滤器
            log.info("==> 全量同步用户点赞列表到布隆过滤器, 用户ID: {}, 过期时间: {}", userId, expireSeconds);
            List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectByUserId(userId);
            if (CollUtil.isNotEmpty(noteLikeDOS)) {  // 存在点赞记录, 则初始化布隆过滤器
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_like_and_expire.lua")));
                script.setResultType(Long.class);
                // 构建LUA参数
                List<Object> luaArgs = Lists.newArrayList();
                noteLikeDOS.forEach(noteLikeDO -> {
                    luaArgs.add(noteLikeDO.getNoteId());
                });
                luaArgs.add(expireSeconds);
                log.info("luaArgs: {}", luaArgs);
                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), luaArgs.toArray());
            }
        } catch (Exception e) {
            log.error("==> 异步初始化布隆过滤器失败", e);
        }
    }

    /**
     * 构建 ZSET 点赞列表的 LUA 参数
     * @param noteLikeDOS
     * @param expireSeconds
     * @return
     */
    private static Object[] buildNoteLikeZSetLuaArgs(List<NoteLikeDO> noteLikeDOS, long expireSeconds) {
        int argsLength = noteLikeDOS.size() * 2 + 1; // 每个笔记点赞关系有 2 个参数（score 和 value），最后再跟一个过期时间
        Object[] luaArgs = new Object[argsLength];

        int i = 0;
        for (NoteLikeDO noteLikeDO : noteLikeDOS) {
            luaArgs[i] = DateUtils.localDateTime2Timestamp(noteLikeDO.getCreateTime()); // 点赞时间作为 score
            luaArgs[i + 1] = noteLikeDO.getNoteId();          // 笔记ID 作为 ZSet value
            i += 2;
        }

        luaArgs[argsLength - 1] = expireSeconds; // 最后一个参数是 ZSet 的过期时间
        return luaArgs;
    }

    /**
     * 异步初始化用户点赞列表 ZSET
     * @param userId
     * @param userNoteLikeZSetKey
     */
    private void asyncInitUserNoteLikesZSet(Long userId, String userNoteLikeZSetKey) {
        taskExecutor.submit(() -> {
            // 判断用户笔记点赞 ZSET 是否存在
            boolean hasKey = redisTemplate.hasKey(userNoteLikeZSetKey);
            if (!hasKey) {   // 不存在, 则初始化
                List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectLikedByUserIdAndLimit(userId, 100);
                if (CollUtil.isNotEmpty(noteLikeDOS)) {
                    long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);  // 12小时 + 随机秒数
                    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                    script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
                    script.setResultType(Long.class);
                    // 构建LUA参数
                    Object[] luaArgs = buildNoteLikeZSetLuaArgs(noteLikeDOS, expireSeconds);
                    log.info("luaArgs: {}", luaArgs);
                    redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), luaArgs);
                }
            }
        });
    }

    /**
     * 初始化 笔记收藏布隆过滤器Key 并设置过期时间
     * @param userId
     * @param expireSeconds
     * @param bloomNoteCollectKey
     */
    private void batchAddNoteCollection2BloomAndExpire(Long userId, long expireSeconds, String bloomNoteCollectKey) {
        try {
            log.info("==> 全量同步用户收藏笔记列表到布隆过滤器, 用户ID: {}, 过期时间: {}", userId, expireSeconds);
            List<NoteCollectionDO> noteCollectionDOS = noteCollectionDOMapper.selectByUserId(userId);
            if (CollUtil.isNotEmpty(noteCollectionDOS)) {
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/collectNote/bloom_batch_add_note_collect_and_expire.lua")));
                script.setResultType(Long.class);
                // 构建LUA参数
                List<Object> luaArgs = Lists.newArrayList();
                noteCollectionDOS.forEach(noteCollectionDO -> {
                    luaArgs.add(noteCollectionDO.getNoteId());
                });
                luaArgs.add(expireSeconds);
                log.info("luaArgs: {}", luaArgs);
                redisTemplate.execute(script, Collections.singletonList(bloomNoteCollectKey), luaArgs.toArray());
            }
        } catch (Exception e) {
            log.error("==> 异步初始化布隆过滤器 [笔记收藏] 失败", e);
        }

    }

    /**
     * 异步初始化用户收藏笔记列表 ZSET
     * @param userId
     * @param userNoteCollectZSetKey
     */
    private void asyncInitUserNoteCollectZSet(Long userId, String userNoteCollectZSetKey) {
        taskExecutor.submit(() -> {
            Boolean hasKey = redisTemplate.hasKey(userNoteCollectZSetKey);
            log.info("笔记收藏ZSET结果: {}", hasKey);
            if (!hasKey) {   // 不存在, 则初始化
                List<NoteCollectionDO> dos = noteCollectionDOMapper.selectCollectedByUserIdAndLimit(userId, 100);
                log.info("dos: {}", dos);
                if (CollUtil.isNotEmpty(dos)) {
                    long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);  // 12小时 + 随机秒数
                    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                    script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/collectNote/batch_add_note_collect_zset_and_expire.lua")));
                    script.setResultType(Long.class);
                    Object[] luaArgs = buildNoteCollectZSETLuaArgs(dos, expireSeconds);
                    redisTemplate.execute(script, Collections.singletonList(userNoteCollectZSetKey), luaArgs);
                }
            }
        });
    }

    private static Object[] buildNoteCollectZSETLuaArgs(List<NoteCollectionDO> noteCollectionDOS, long expireSeconds) {
        int argsLength = noteCollectionDOS.size() * 2 + 1;
        Object[] luaArgs = new Object[argsLength];

        int i = 0;
        for (NoteCollectionDO noteCollectionDO : noteCollectionDOS) {
            luaArgs[i] = DateUtils.localDateTime2Timestamp(noteCollectionDO.getCreateTime());
            luaArgs[i + 1] = noteCollectionDO.getNoteId();
            i += 2;
        }

        luaArgs[argsLength - 1] = expireSeconds;
        return luaArgs;
    }

    /**
     * 点赞笔记, 通过 roaring bitmap校验是否点赞
     * @param likeNoteReqVO
     * @return
     */
    public Response<?> likeNote_checkByRoaringBitmap(LikeNoteReqVO likeNoteReqVO) {
        Long userId = LoginUserContextHolder.getUserId();
        Long noteId = likeNoteReqVO.getNoteId();
        //1. 本地Caffeine缓存+数据库 查询 校验笔记是否存在
        Long creatorId = checkNoteIsExistsAndGetAuthorId(noteId);
        //2. 校验用户是否已经点赞 (rbitmap查询key)
        String rbitmapUserNoteLikeListKey = RedisKeyConstants.buildRBitmapUserNoteLikeListKey(userId);  // rbitmap:note:likes:userId

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/likeCheck/rbitmap_note_like_check.lua")));
        script.setResultType(Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId);   // 执行lua脚本
        NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);

        String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);    // user:note:likes:2
        switch (noteLikeLuaResultEnum) {    // 点赞的笔记id不存在时
            case NOT_EXISTS -> {
                // 从数据库中校验笔记是否被点赞，并异步初始化 Roaring Bitmap，设置过期时间
                int count = noteLikeDOMapper.selectCountByUserIdAndNoteId(userId, noteId);
                // 半天 + 随机秒数
                long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);
                if (count > 0) {
                    taskExecutor.submit(() -> {
                        batchAddNoteLike2RbitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey);
                    });
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
                // 数据库中不存在当前笔记的点赞记录, 同步初始化 Roaring Bitmap，设置过期时间
                batchAddNoteLike2RbitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey);

                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/likeCheck/rbitmap_add_note_like_and_expire.lua")));
                // 返回值类型
                script.setResultType(Long.class);
                redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId, expireSeconds);
            }
            case NOTE_LIKED -> {    // 笔记已经被点赞
                throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
            }
        }
        //3. 更新用户 ZSET 点赞列表
        LocalDateTime now = LocalDateTime.now();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_like_check_and_update_zset.lua")));    // 校验笔记是否在ZSET中存在, 存在就更新点赞
        script.setResultType(Long.class);
        result = redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
        if (Objects.equals(result, NoteLikeLuaResultEnum.NOT_EXISTS.getCode())) {   // ZSET 未存在, 需要初始化
            // 数据库查询最近点赞的100条笔记, 批量添加到ZSET
            List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectLikedByUserIdAndLimit(userId, 100);
            long expireSeconds = 60 * 60 * 12 + RandomUtil.randomInt(60 * 60);  // 12小时 + 随机秒数
            DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
            script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
            script2.setResultType(Long.class);
            if (CollUtil.isNotEmpty(noteLikeDOS)) {
                Object[] luaArgs = buildNoteLikeZSetLuaArgs(noteLikeDOS, expireSeconds);
                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs);    // 同步点赞记录(数据库)到ZSET
                redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now)); // 将点赞的笔记添加到ZSET
            } else {    // 数据库中没有点赞记录, 则初始化 ZSET 添加一条点赞记录
                List<Object> luaArgs = Lists.newArrayList();
                luaArgs.add(DateUtils.localDateTime2Timestamp(LocalDateTime.now()));    // score 时间戳
                luaArgs.add(noteId);
                luaArgs.add(expireSeconds);
                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs.toArray());
            }
        }
        //4. 发送MQ 点赞数据入库
        LikeUnLikeNoteMqDTO likeUnLikeNoteMqDTO = LikeUnLikeNoteMqDTO.builder()
                .userId(userId)
                .noteCreatorId(creatorId)
                .noteId(noteId)
                .type(LikeUnLikeNoteTypeEnum.LIKE.getCode())    // 点赞
                .createTime(LocalDateTime.now())
                .build();
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnLikeNoteMqDTO)).build();
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_LIKE; // TOPIC 携带 TAG
        String hashKey = String.valueOf(userId);
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("LikeOrUnlikeNote MQ 点赞数据 发送成功, message: {}", message);
            }

            @Override
            public void onException(Throwable throwable) {
                log.info("LikeOrUnlikeNote MQ 点赞数据 发送失败, error: {}", throwable);
            }
        });

        return Response.success();
    }

    private void batchAddNoteLike2RbitmapAndExpire(Long userId, long expireSeconds, String rbitmapUserNoteLikeListKey) {
        try {
            //1. 异步全量同步用户点赞列表到咆哮位图
            log.info("==> 全量同步用户点赞列表到 咆哮位图, 用户ID: {}, 过期时间: {}", userId, expireSeconds);
            List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectByUserId(userId);
            if (CollUtil.isNotEmpty(noteLikeDOS)) {  // 存在点赞记录, 则初始化布隆过滤器
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/likeCheck/rbitmap_batch_add_note_like_and_expire.lua")));
                script.setResultType(Long.class);
                // 构建LUA参数
                List<Object> luaArgs = Lists.newArrayList();
                noteLikeDOS.forEach(noteLikeDO -> {
                    luaArgs.add(noteLikeDO.getNoteId());
                });
                luaArgs.add(expireSeconds);
                log.info("luaArgs: {}", luaArgs);
                redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), luaArgs.toArray());
            }
        } catch (Exception e) {
            log.error("==> 异步初始化咆哮位图 [用户点赞] 失败", e);
        }
    }

}
